<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8"/>
  <title>货物分拣 3D 演示 - TTS</title>
  <style>
      body { margin: 0; overflow: hidden; font-family: Arial, sans-serif; }
      #info { position: absolute; top: 10px; left: 10px; color:#fff; background: rgba(0,0,0,.6); padding: 8px 12px; border-radius:4px; }
  </style>
</head>
<body>
  <div id="info">
      <button id="btnPlay">暂停</button>
      <button id="btnSpeed">x1</button>
	  <span id="total">货物总数:0</span>
      <span id="stats">等待货物…</span>
  </div>

  <!-- CDN 引入，可换成本地文件 -->
  <script src="ThreeJs/js/three.js"></script>
  <script src="ThreeJs/js/OrbitControls.js"></script>
  
  <script type="module">
	
	const beltW=1;//输送线宽
	/* ---------- 分拣环 ---------- */
	const W=5,L=30;
	// 中点坐标
	const mf=[0,0,0];
	const mb=[0,0,-W];
	const mr=[  L/2-beltW/2, 0, -W/2];
	const ml=[-(L/2-beltW/2),0, -W/2];
	// 四角坐标 左下,右下,右上,左上
	const e0=[-L/2+beltW/2, 0, 0];
	const e1=[ L/2-beltW/2, 0, 0];
	const e2=[ L/2-beltW/2, 0, -W];
	const e3=[-L/2+beltW/2, 0, -W];
	
	const len_imp =2;
	/* 货物输送带 以端点来计算 */
	const len_conveyor = 8;
	const beltsCV = [
		{name:'CV-B-8',   type:'conveyor', len:len_conveyor, pos:[-8, 0, -W-len_imp-len_conveyor/2             ], rot:Math.PI/2, endpoints:[[-8,0,-W-len_imp-len_conveyor  ],[-8,0,-W-len_imp             ]], dir:[ 0,0, 1]}
	];
	/* 货物进入段 */
	const beltsIn = [
		
		{name:'CV-B-8-1', type:'conveyor', len:len_conveyor, pos:[-8-(len_conveyor*Math.cos(Math.PI/4))/2, 0, -W-len_imp-len_conveyor-(len_conveyor*Math.cos(Math.PI/4))/2 ], rot:-Math.PI/4, endpoints:[[-8-(len_conveyor*Math.cos(Math.PI/4)),0,-W-len_imp-len_conveyor-(len_conveyor*Math.cos(Math.PI/4))],[-8,0,-W-len_imp-len_conveyor]], dir:[ 1,0, 1]},
		{name:'CV-B-8-2', type:'conveyor', len:len_conveyor, pos:[-8, 0, -W-len_imp-len_conveyor-len_conveyor/2], rot:Math.PI/2, endpoints:[[-8,0,-W-len_imp-len_conveyor*2],[-8,0,-W-len_imp-len_conveyor]], dir:[ 0,0, 1]}
	];
	
	// 货物颜色与分拣口颜色对应,方便查看
	const colors = [0xff0000, 0x00ff00, 0x0000ff,0xffaa00, 0x00ffaa, 0xaa00ff];
	
	const belts = [...beltsCV,...beltsIn];
	
	/* ---------- adj邻接表 divert分流,merge汇流,branch/side支线/侧边进入---------- */
	const edges = [
		{ from:"CV-B-8-1", to:"CV-B-8" ,type: "merge" },
		{ from:"CV-B-8-2", to:"CV-B-8" ,type: "merge" },
		{ from:"CV-B-8", to:"IMP-B-8" }
	];
	
	// 构建图结构 - 初始化时执行一次
	const graph = (function buildGraph(edges) {
		const graph = {};
		// 初始化所有节点
		edges.forEach(edge => {
			if (!graph[edge.from]) {
				graph[edge.from] = [];
			}
			if (!graph[edge.to]) {
				graph[edge.to] = [];
			}
		});
		// 添加双向连接
		edges.forEach(edge => {
			graph[edge.from].push(edge.to);
		});
		return graph;
	})(edges); // 立即执行函数，初始化时就构建图
	// 寻找从起点到终点的路径
	function findRoute(start, end) {
		if (!graph[start] || !graph[end]) {
			return null; // 起点或终点不存在
		}
		
		// BFS算法实现
		const queue = [[start]];
		const visited = new Set();
		visited.add(start);
		
		while (queue.length > 0) {
			// 取出队列中的第一条路径
			const path = queue.shift();
			// 取出路径的最后一个节点
			const current = path[path.length - 1];
			// 如果到达终点，返回路径
			if (current === end) {
				return path;
			}
			// 遍历当前节点的所有邻居
			for (const neighbor of graph[current]) {
				if (!visited.has(neighbor)) {
					visited.add(neighbor);
					// 创建新路径并加入队列
					const newPath = [...path, neighbor];
					queue.push(newPath);
				}
			}
		}
		// 没有找到路径
		return null;
	}
  
  
	const scene    = new THREE.Scene();
	scene.background = new THREE.Color(0x202020);
	const camera   = new THREE.PerspectiveCamera(60, innerWidth/innerHeight, 0.1, 1000);
	camera.position.set(5, 10, 15); // 调整相机位置以便更好地观察
	const renderer = new THREE.WebGLRenderer({ antialias: true });
	renderer.setSize(innerWidth, innerHeight);
	renderer.shadowMap.enabled = true;
	document.body.appendChild(renderer.domElement);

	const ctrl = new THREE.OrbitControls(camera, renderer.domElement);
	ctrl.enableDamping = true;

	/* ---------- 灯光 ---------- */
	const amb = new THREE.AmbientLight(0xffffff, 0.6);
	scene.add(amb);
	const dir = new THREE.DirectionalLight(0xffffff, 1);
	dir.position.set(5,10,7);
	dir.castShadow = true;
	scene.add(dir);

	/* ---------- 地面 ---------- */
	const groundGeo = new THREE.PlaneGeometry(30,30);
	const groundMat = new THREE.MeshStandardMaterial({ color: 0x333333 });
	const ground = new THREE.Mesh(groundGeo, groundMat);
	ground.rotation.x = -Math.PI/2;
	ground.receiveShadow = true;
	scene.add(ground);

	/* 显示坐标轴（红 X、绿 Y、蓝 Z） */
	const axes = new THREE.AxesHelper(10);
	//scene.add(axes);

	/* ---------- 传送带类 ---------- */
	class Belt {
		constructor(data, colors = [0x555555, 0x00ff00]) {
			//colors:[传送带颜色,中心线颜色]
			this.data = data;
			const x=data.pos[0], y=data.pos[1], z=data.pos[2];
			const geo = new THREE.BoxGeometry(data.len, 0.1, beltW);
			const mat = new THREE.MeshStandardMaterial({ color: colors[0] });
			const mesh = new THREE.Mesh(geo, mat);
			mesh.position.set(x, y, z);
			mesh.rotation.y = data.rot;
			mesh.receiveShadow = mesh.castShadow = false;
			scene.add(mesh);
			
			const pts = [[data.endpoints[0][0],0.11,data.endpoints[0][2]],[data.endpoints[1][0],0.11,data.endpoints[1][2]]];
			
			const lineGeo = new THREE.BufferGeometry().setFromPoints( pts.map(p=>new THREE.Vector3(...p)) );
			const lineMat = new THREE.LineBasicMaterial({ color: colors[1] });
			const line = new THREE.Line(lineGeo, lineMat);
			scene.add(line);
		}
	}
	
	beltsCV.forEach((o,idx)=>{
		new Belt(o);
	});
	beltsIn.forEach((o,idx)=>{
		new Belt(o);
	});

	/* ---------- 包裹类 ---------- */
	class Parcel {
		constructor(id, routes,color) {
			this.id = id;
			this.start   = routes[0];
			this.setRoutes(routes);
			this.setCurrent(routes[0]);
			this.speedFactor = 1; // 新增：速度因子，0表示停止，1表示正常移动
			this.isMoving = true; // 新增：是否正在移动
			const mat = new THREE.MeshStandardMaterial({ color: color });
			this.mesh = new THREE.Mesh(new THREE.BoxGeometry(0.6,0.6,0.6), mat);
			this.mesh.castShadow = false;
			this.mesh.position.set(this.start.endpoints[0][0], 0.4, this.start.endpoints[0][2]);
			scene.add(this.mesh);
		}
		setRoutes(routes){
			//修改路由
			this.end = routes[routes.length-1];	 // 终点:目标出口
			this.routes= routes;				 //路由点
		}
		setCurrent(current){
			this.current = current;
			const idx = this.routes.findIndex(item => item.name === current.name);
			if (idx<0) return;
			if(idx === this.routes.length-1) this.next = null;
			else this.next = this.routes[idx+1];
		}
		
		distance(p1,p2){
			//2点间距离<0.12表示到达该位置, 是否优化为二维坐标的距离(x,z)
			return Math.abs(p1 -p2) < 0.08;
		}
		checkCollision(otherParcel) {
			const dist = this.mesh.position.distanceTo(otherParcel.mesh.position);
			return dist < 0.8; // 小于0.8视为即将碰撞
		}
		update(delta) {
			if (!this.isMoving) return false;
			const distance_move = 2 * delta * speed; //每次刷新时移动的距离
			if (this.current.dir[0] ===  1) this.mesh.position.x += distance_move;
			if (this.current.dir[1] ===  1) this.mesh.position.y += distance_move;
			if (this.current.dir[2] ===  1) this.mesh.position.z += distance_move;
			if (this.current.dir[0] === -1) this.mesh.position.x -= distance_move;
			if (this.current.dir[1] === -1) this.mesh.position.y -= distance_move;
			if (this.current.dir[2] === -1) this.mesh.position.z -= distance_move;
			
			
			// 到达段末尾
			if ( (this.current.dir[0] ===  1 && this.mesh.position.x >= this.current.endpoints[1][0]) 
			  || (this.current.dir[1] ===  1 && this.mesh.position.y >= this.current.endpoints[1][1])
			  || (this.current.dir[2] ===  1 && this.mesh.position.z >= this.current.endpoints[1][2])
			  || (this.current.dir[0] === -1 && this.mesh.position.x <= this.current.endpoints[1][0])
			  || (this.current.dir[1] === -1 && this.mesh.position.y <= this.current.endpoints[1][1])
			  || (this.current.dir[2] === -1 && this.mesh.position.z <= this.current.endpoints[1][2])
			){
				if(!this.next){
					scene.remove(this.mesh);
					return true; // 完成,包裹移除
				}
				this.setCurrent(this.next);
			}
			return false;
		}
	}

	/* ---------- 主循环 ---------- */
	const parcels = new Map();
	let paused = false;
	let speed  = 1;
	const clock = new THREE.Clock();

	function animate() {
		requestAnimationFrame(animate);
		if (!paused) {
			const delta = clock.getDelta();
			if(delta>0.1) return; //浏览器切换到后台,则不运行动画; 后台运行的话,包裹的位置可能超出了输送线
			
			// 1. 先重置所有包裹的移动状态
			parcels.forEach(p => {
			  p.speedFactor = 1;
			  p.isMoving = true;
			});

			// 2. 检测碰撞并设置等待状态
			const parcelList = Array.from(parcels.values());
			for (let i = 0; i < parcelList.length; i++) {
			  const current = parcelList[i];
			  // 检查当前包裹是否需要等待
			  for (let j = 0; j < parcelList.length; j++) {
				if (i === j) continue;
				const other = parcelList[j];
				
				// 只检测同一输送段或前后衔接输送段的包裹
				const sameOrNext = current.current === other.current || 
								  (current.next && current.next === other.current);
				
				if (sameOrNext && current.checkCollision(other)) {
				  // 后面的包裹需要等待
				  const isBehind = checkIsBehind(current, other);
				  if (isBehind) {
					current.speedFactor = 0;
					current.isMoving = false;
					break; // 找到一个碰撞对象即可
				  }
				}
			  }
			}

			// 3. 更新所有包裹位置
			
			for (let [id, p] of parcels) {
				const finished = p.update(delta);
				if (finished) parcels.delete(id);
			}
			document.getElementById('stats').textContent = `在线货物: ${parcels.size}`;
		}
		ctrl.update();
		renderer.render(scene, camera);
	}
	animate();
	
	
	// 检查current是否在other后面（根据输送方向）
	function checkIsBehind(current, other) {
	  const dir = current.current.dir;
	  const posCurrent = current.mesh.position;
	  const posOther = other.mesh.position;

	  // 根据传送带方向判断前后关系
	  if (dir[0] === 1) return posCurrent.x < posOther.x; // 沿X正方向
	  if (dir[0] === -1) return posCurrent.x > posOther.x; // 沿X负方向
	  if (dir[2] === 1) return posCurrent.z < posOther.z; // 沿Z正方向
	  if (dir[2] === -1) return posCurrent.z > posOther.z; // 沿Z负方向
	  return false;
	}


	/* ---------- 控制按钮 ---------- */
	document.getElementById('btnPlay').onclick = () => {
		paused = !paused;
		if(paused) clock.stop(); else clock.start();
		document.getElementById('btnPlay').textContent = paused ? '播放' : '暂停';
	};
	document.getElementById('btnSpeed').onclick = () => {
		speed = speed === 1 ? 2 : speed === 2 ? 4 : speed === 4 ? 8 : speed === 8 ? 0.5 : 1;
		document.getElementById('btnSpeed').textContent = `x${speed}`;
	};

	/* ---------- 自动生成测试数据 ---------- */
	function createParcel(Id,start,end,color){
		const routes = [];
		const paths = findRoute(start, end); //路由
		//console.log(paths);
		paths.forEach(x => {
			routes.push(belts.find(y => y.name === x));
		});
		parcels.set(Id, new Parcel(Id, routes,color));
		document.getElementById('total').textContent = `货物总数: ${Id} `;
	}
	let testId = 0;
	function generateParcel(){
	  if (!paused && parcels.size < beltsCV.length*10) {
		const impIdx = testId%2;
		const targetIdx = Math.floor(Math.random()*beltsCV.length);
		const color = colors[targetIdx];
		
		// 检查起始段是否已有太多包裹
		const startBelt = beltsIn[impIdx].name;
		const startParcels = Array.from(parcels.values())
		  .filter(p => p.current.name === startBelt).length;
		  
		// 每个进入段最多同时存在5个包裹
		if (startParcels < 10) {
		  createParcel(testId, beltsIn[impIdx].name, beltsCV[targetIdx].name, color);
		  testId++;
		}
	  }
	}
	setInterval(()=>generateParcel(), 200);

	/* ---------- 窗口适配 ---------- */
	addEventListener('resize', ()=>{
	  camera.aspect = innerWidth/innerHeight;
	  camera.updateProjectionMatrix();
	  renderer.setSize(innerWidth, innerHeight);
	});
  </script>
</body>
</html>